# gulp手写注入插件

# 背景

因项目需要对打包文件加hash值做版本控制,于是使用gulp-rev进行hash值的生成,看官方文档,要想将生成的文件注入到需要的地方,需要使用rev.manifest生成个json文件,再从json文件中通过记录找出对应的hash值。

写法如下:

const gulp = require('gulp');
const rev = require('gulp-rev');

exports.default = () => (
	// By default, Gulp would pick `assets/css` as the base,
	// so we need to set it explicitly:
	gulp.src(['assets/css/*.css', 'assets/js/*.js'], {base: 'assets'})
		.pipe(gulp.dest('build/assets'))  // 复制原资产build目录
		.pipe(rev())
		.pipe(gulp.dest('build/assets'))  // 写rev的资产build目录
		.pipe(rev.manifest())
		.pipe(gulp.dest('build/assets'))  // 写入清单以build目录
);

因为存在多个打包入口,使用上述写法rev.manifest会被后执行的覆盖,所以得使用merge选项进行合并。

.pipe(rev.manifest({
	base: 'build/assets',
	merge: true // 与现有清单合并(如果存在)
}))

在使用了上述写法后,发现当打包的文件有变化时,生成的json映射文件中的内容却没有变化,网上也没找到类似的状况,加上gulp的注入插件gulp-inject在针对php文件时没生效,于是决定自己写一个注入插件。

# 实现

# 流处理

gulp插件就是对文件流进行处理

所以使用through2来处理

var through2 = require('through2');

/**
 * 注入
 * @param {*} options 参数:
 * store: 用于存储文件名及语言类型的数组
 * index: 文件的加载顺序(文件大小不同,存入的顺序不同,所以要根据所以存)
 * fileNumber: 总文件数(用于判断结束)
 * injectStartKey: 用于注入时判断注入开始的标识
 * injectEndKey: 用于注入时判断注入结束的标识(在开始与结束之间的内容将被替换)
 * injectFilesPath: 要被注入的文件路径,可以是字符串或者数组
 * @returns 
 */
function inject (options) {
	return through2.obj(function (file, encoding, cb) {
		recordFileData(file, options);
		cb(null, file);
	});
}

module.exports = inject;

将当前流文件传入方法recordFileData中进行数据的记录。

处理完成后调callback函数让gulp继续走下去。

# 记录文件数据

下面来看下主要的处理逻辑

// 生成hash值后的回调,因为是异步的,所以得每次判断长度,全部执行完时再执行注入操作
function recordFileData (file, options) {
	var nameArr = file.history[file.history.length - 1].split('\\');
	var store = options.store;
	store[options.index] = {
	  name: nameArr[nameArr.length - 1],
	  language: options.language
	};
	if (store.length === options.fileNumber) {
	  injectStaticFile(options);
	}
}

因为gulp-rev处理后的文件会带有history属性,其中最后一个就是带hash值的文件名(绝对路径),将文件名截出来,并且和文件的语言类型记录到传入的store中,用于后面的文件注入。

之前在options中有传文件总数量和该文件在总文件中的顺序索引(因为存在多个打包入口,根据文件大小,打包完成时间不会按照顺序来,所以这里用索引来记录顺序),判断store的总长度等于文件总数时,即认为全部文件都存入完成,开始注入。

# 注入

options中取出注入的标识injectStartKeyinjectEndKey以及需要替换的文件路径injectFilesPath

循环store,拼接资源链接字符串。

如果是有语言区分的(中英文环境),需要用php变量进行判断,要注意使用php代码时字符串要转义。

拼好字符串后根据分隔符插入字符串,并写入到文件中,完成了需求。

看代码:

// 向文件注入文件名
function injectStaticFile (options) {
	var store = options.store
	var injectStartKey = options.injectStartKey;
	var injectEndKey = options.injectEndKey;
	// 需要替换的文件路径数组
	var injectFilesPath = options.injectFilesPath;
	// 传的是字符串的话转成数组
	if (typeof injectFilesPath === 'string') {
		injectFilesPath = [injectFilesPath];
	}
  
	var fileArr = [];
	var lineFeed = '\n';
	
	store.forEach(function (item) {
	  var str = '';
	  if (item.language) {
		str = '<?php' +
		  ' if ($language === "' + item.language + '") {' +
			'echo "' + generateFilesLink(item, true) + '";' +
		  ' }' +
		' ?>'
	  } else {
		str = generateFilesLink(item, false);
	  }
	  fileArr.push(str);
	});
	// 拼出需引用资源的链接的字符串
	var linkStr = lineFeed + fileArr.join(lineFeed) + lineFeed;
  
	injectFilesPath.forEach(function(phpFilePath) {
	  injectStaticFileSingle(phpFilePath, linkStr, injectStartKey, injectEndKey);
	})
}
  
  // 向单个文件注入文件名
function injectStaticFileSingle (phpFilePath, linkStr, injectStartKey, injectEndKey) {
	var phpFile = fs.readFileSync(path.resolve(__dirname, phpFilePath), 'utf8');
	var injectStartArr = phpFile.split(injectStartKey);
	var injectStart = injectStartArr[0];
	var injectEndArr = injectStartArr[1].split(injectEndKey);
	var injectEnd = injectEndArr[1];
	
	phpFile = injectStart + injectStartKey + linkStr + injectEndKey + injectEnd;
	fs.writeFileSync(path.resolve(__dirname, phpFilePath), phpFile, 'utf8');
}
  
// php里的字符要转义
function generateFilesLink (item, inPhp) {
	if (inPhp) {
	  if (item.name.indexOf('.js') !== -1) {
		return "<script type=\\\"text/javascript\\\" src=\\\"./static/" + item.name + "\\\"></script>";
	  } else {
		return "<link rel=\\\"stylesheet\\\" type=\\\"text/css\\\" href=\\\"./static/"+ item.name + "\\\" />";
	  }
	} else {
	  if (item.name.indexOf('.js') !== -1) {
		return '<script type="text/javascript" src="./static/' + item.name + '"></script>';
	  } else {
		return '<link rel="stylesheet" type="text/css" href="./static/'+ item.name + '" />';
	  }
	}
}